package npr;

import processing.core.*;

import javax.media.opengl.*;
import com.sun.opengl.util.*;

import java.nio.ShortBuffer;

/** 
 * Renders line drawings by running a soble edge detection filter on a depth map and a normal map
 */
public class SobelLines extends ImageSpaceLines{

    /** 
     * Shader for rendering the scene with a red light from the left
     * a green light from the front, and a blue light from the top.
     */
    private GLSL leftFrontTopShader;
    /** 
     * Shader for rendering the scene with a red light from the right
     * a green light from the back, and a blue light from the bottom.
     */
    private GLSL rightBackBottomShader;
    /** 
     * Shader for 3x3 gaussian filtering 
     */ 
    private GLSL gaussian3x3;
    /** 
     * Shader for 5x5 gaussian filtering 
     */ 
    private GLSL gaussian5x5;
    /** 
     * Shader for sobel filtering without any additional rendering passes. 
     * Line strength is coded as alpha. 
     */
    private GLSL sobelToScreen;
    /**
     * Shader for sobel filtering with additional rendering passes. 
     * Line strength is coded in the color channel, as reading back the 
     * alpha values in GLSL didn't work out. :( 
     */
    private GLSL sobelToTexture;
    /** 
     * Shader for dilation filtering (broadening lines) with a 3x3 filter kernel.
     * This is in addition to the filter for the general case, because some older 
     * graphics cards have problems with larger filter kernels.
     */
    private GLSL dilation3x3;
    /** 
     * Shader for dilation filtering (broadening lines) with a 5x5 filter kernel.
     * This is in addition to the filter for the general case, because some older 
     * graphics cards have problems with larger filter kernels.
     */    
    private GLSL dilation5x5;
    /** 
     * Shader for dilation filtering (broadening lines) for kernel sizes other than 3x3 or 5x5.
     */
    private GLSL dilation;
    /** 
     * Texture holding the result of rendering the scene with a red light from the left
     * a green light from the front, and a blue light from the top.
     */
    private RenderToTexture leftFrontTopTexture;
    /** 
     * Texture holding the result of rendering the scene with a red light from the right
     * a green light from the back, and a blue light from the bottom.
     */
    private RenderToTexture rightBackBottomTexture;
    /**
     * Texture holding the result of running a gaussian filter over leftFrontTopTexture.
     */
    private RenderToTexture leftFrontTopGaussian;
    /**
     * Texture holding the result of running a gaussian filter over rightBackBottomTexture.
     *
     */
    private RenderToTexture rightBackBottomGaussian; 
    /**
     * Texture holding the lines detected by sobel filtering.
     * Needed for the additional rendering pass of dilation.
     */
    private RenderToTexture lines;

    /**
     * The start value for smoothstep interpolation.
     */
    protected float smoothstepStart= 0.0f;
    /**
     * The end value for smoothstep interpolation.
     */
    protected float smoothstepEnd= 1.0f;

    /** 
     * The filter kernel size for gaussian filtering. Allowed values are 3 and 5.
     */
    private int gaussianSize= 3;

    /**
     * Specifies whether to run gaussian filtering.
     */
    private boolean useGaussian= false;

    /**
     * Specifies whether to use smoothstep interpolation.
     */
    private boolean useSmoothstep= false;

    /**
     * Creates a SobelLines object and initializes it.
     */
    public SobelLines(){

        leftFrontTopTexture= new RenderToTexture();
	rightBackBottomTexture= new RenderToTexture();
	leftFrontTopGaussian= new RenderToTexture();
	rightBackBottomGaussian= new RenderToTexture();
	lines= new RenderToTexture();

        leftFrontTopShader=new GLSL();
        leftFrontTopShader.loadVertexShader("leftFrontTopShader.vert");
        leftFrontTopShader.loadFragmentShader("leftFrontTopShader.frag");
        leftFrontTopShader.useShaders();
	
        rightBackBottomShader=new GLSL();
        rightBackBottomShader.loadVertexShader("rightBackBottomShader.vert");
        rightBackBottomShader.loadFragmentShader("rightBackBottomShader.frag");
        rightBackBottomShader.useShaders();

	gaussian3x3=new GLSL();
        gaussian3x3.loadVertexShader("gaussian.vert");
        gaussian3x3.loadFragmentShader("gaussian3x3.frag");
        gaussian3x3.useShaders();

	gaussian5x5=new GLSL();
        gaussian5x5.loadVertexShader("gaussian.vert");
        gaussian5x5.loadFragmentShader("gaussian5x5.frag");
        gaussian5x5.useShaders();

    	sobelToScreen=new GLSL();
        sobelToScreen.loadVertexShader("sobel.vert");
        sobelToScreen.loadFragmentShader("sobelToScreen.frag");
        sobelToScreen.useShaders();

    	sobelToTexture=new GLSL();
        sobelToTexture.loadVertexShader("sobel.vert");
        sobelToTexture.loadFragmentShader("sobelToTexture.frag");
        sobelToTexture.useShaders();

	dilation3x3=new GLSL();
        dilation3x3.loadVertexShader("dilation.vert");
        dilation3x3.loadFragmentShader("dilation3x3.frag");
        dilation3x3.useShaders();

	dilation5x5=new GLSL();
        dilation5x5.loadVertexShader("dilation.vert");
        dilation5x5.loadFragmentShader("dilation5x5.frag");
        dilation5x5.useShaders();
	
	dilation=new GLSL();
        dilation.loadVertexShader("dilation.vert");
        dilation.loadFragmentShader("dilation.frag");
        dilation.useShaders();

	width= 1.0f;
	
    }

    /**
     * Specifies whether to run gaussian filtering before sobel edge detection.
     * This suppresses weaker lines with noisy objects. It also results in
     * a certain broadening of lines and has an antialiasing effect.
     *
     * @param useGaussian true if use gaussian, false if not
     * The default is false.
     *
     * @see npr.SobelLines#setGaussianSize
     */
    public void useGaussian(boolean useGaussian){

	this.useGaussian= useGaussian;
    }

    /**
     * Sets the filter kernel size for gaussian filtering. Accepted values are 3 and 5.
     *
     * @param size size of the filter kernel
     *
     * @see npr.SobelLines#useGaussian
     */
    public void setGaussianSize(int size){

	if(size<3){
	    gaussianSize= 3;
	    System.err.println("Gaussian size must be 3 or 5");
	}
	else if(size>5){
	    gaussianSize= 5;
	    System.err.println("Gaussian size must be 3 or 5");
	}  
	else if(size==4){
	    gaussianSize= 3;
	    System.err.println("Gaussian size must be 3 or 5");
	}
	gaussianSize= size;
    }


    /**
     * Specifies whether to use smoothstep interpolation.
     * 
     * @param useSmoothstep true if smoothstep should be used, false if not
     *
     * The default is false.
     * @see npr.SobelLines#setSmoothstepStart
     * @see npr.SobelLines#setSmoothstepEnd
     */
    public void useSmoothstep(boolean useSmoothstep){

	this.useSmoothstep= useSmoothstep;
    }

    /**
     * Sets the start value for smoothstep interpolation.
     *
     * @see npr.SobelLines#useSmoothstep
     * @see npr.SobelLines#setSmoothstepEnd
     */
    public void setSmoothstepStart(float start){

	this.smoothstepStart= start;
    }
    
    /**
     * Sets the end value for smoothstep interpolation.
     *
     * @see npr.SobelLines#useSmoothstep
     * @see npr.SobelLines#setSmoothstepStart
     */
    public void setSmoothstepEnd(float end){

	this.smoothstepEnd= end;
    }

    /**
     * Renders depth and normal maps and possibly runs a gaussian filter.
     * It also runs sobel edge detection and renders it to a texture in case 
     * the lines should be broadened afterwards.
     * You don't have to call this method. It is called by the renderer.
     */
    public void preProcess(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;
	
	leftFrontTopTexture.begin();

        leftFrontTopShader.startShader();
	render();
        leftFrontTopShader.endShader();

	leftFrontTopTexture.end();
	

	rightBackBottomTexture.begin();

        rightBackBottomShader.startShader();
	render();
        rightBackBottomShader.endShader();

	rightBackBottomTexture.end();

	if(useGaussian){

	    int location;

	    leftFrontTopGaussian.begin();
	    leftFrontTopTexture.bindColorTexture();
	    if(gaussianSize==3) gaussian3x3.startShader();
	    else gaussian5x5.startShader();
	    if(gaussianSize==3){
		location= gaussian3x3.getUniformLocation("screenSize");
		gaussian3x3.setFloatVec2(location, leftFrontTopTexture.getWidth(), leftFrontTopTexture.getHeight());
	    }
	    else{
		location= gaussian5x5.getUniformLocation("screenSize");
		gaussian5x5.setFloatVec2(location, leftFrontTopTexture.getWidth(), leftFrontTopTexture.getHeight());
	    }
	    renderer.fullScreenQuad(1.0f);
	    if(gaussianSize==3) gaussian3x3.endShader();
	    else gaussian5x5.endShader();
	    leftFrontTopTexture.unbind();
	    leftFrontTopGaussian.end();

	    rightBackBottomGaussian.begin();
	    rightBackBottomTexture.bindColorTexture();
	    if(gaussianSize==3) gaussian3x3.startShader();
	    else gaussian5x5.startShader();
	    if(gaussianSize==3){
		location= gaussian3x3.getUniformLocation("screenSize");
		gaussian3x3.setFloatVec2(location, leftFrontTopTexture.getWidth(), leftFrontTopTexture.getHeight());
	    }
	    else{
		location= gaussian5x5.getUniformLocation("screenSize");
		gaussian5x5.setFloatVec2(location, leftFrontTopTexture.getWidth(), leftFrontTopTexture.getHeight());
	    }
	    renderer.fullScreenQuad(1.0f);
	    if(gaussianSize==3) gaussian3x3.endShader();
	    else gaussian5x5.endShader();
	    rightBackBottomTexture.unbind();
	    rightBackBottomGaussian.end();
	}

	if(width>1){ 

	    lines.begin();
	    findLines(true);
	    lines.end();
	}
    }

    /**
     * Runs sobel edge detection. In case the lines should be broadened
     * a dilation filter is run instead.
     * You don't have to call this method. It is called by the renderer.
     */ 
    public void postProcess(){

	renderer= (NPR)parent.g;
	gl= renderer.gl;

	gl.glEnable(GL.GL_DEPTH_TEST);
	gl.glDepthMask(true);

	if(width<=1.0){
	    findLines(false);
	}
	else if(width<=5.0){	    

	    dilation3x3.startShader();

	    int location;

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    rightBackBottomTexture.bindDepthTexture();
	    location= dilation3x3.getUniformLocation("depthTexture");
	    dilation3x3.setTextureUnit(location, 0);

	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    lines.bindColorTexture();
	    location= dilation3x3.getUniformLocation("lines");
	    dilation3x3.setTextureUnit(location, 1);
	    
	    location= dilation3x3.getUniformLocation("screenSize");
	    dilation3x3.setFloatVec2(location, parent.width, parent.height);

	    location= dilation3x3.getUniformLocation("color");
	    dilation3x3.setFloatVec4(location, red, green, blue, alpha);

	    location= dilation3x3.getUniformLocation("threshold");
	    dilation3x3.setInteger(location, (int)(9.0-(width/5.0)*9.0f));

	    if(useGaussian){
		if(gaussianSize==3){
		    location= dilation3x3.getUniformLocation("gaussian");
		    dilation3x3.setInteger(location, 3);
		}  
		else{
		    location= dilation3x3.getUniformLocation("gaussian");
		    dilation3x3.setInteger(location, 5);
		}
	    }
	    else{
		location= dilation3x3.getUniformLocation("gaussian");
		dilation3x3.setInteger(location, 0);
	    }

	    //render a full-screen quad to cover all pixels
	    renderer.fullScreenQuad(1.0f);
	    render();
	    
	    dilation3x3.endShader();
	    
	}
	else if(width<=9.0){
	        
	    gl.glEnable(GL.GL_TEXTURE_2D);	    

	    dilation5x5.startShader();

	    int location;

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    rightBackBottomTexture.bindDepthTexture();
	    location= dilation5x5.getUniformLocation("depthTexture");
	    dilation5x5.setTextureUnit(location, 0);

	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    lines.bindColorTexture();
	    location= dilation5x5.getUniformLocation("lines");
	    dilation5x5.setTextureUnit(location, 1);
	    
	    location= dilation5x5.getUniformLocation("screenSize");
	    dilation5x5.setFloatVec2(location, parent.width, parent.height);

	    location= dilation5x5.getUniformLocation("color");
	    dilation5x5.setFloatVec4(location, red, green, blue, alpha);

	    location= dilation5x5.getUniformLocation("threshold");
	    dilation5x5.setInteger(location, (int)(25-width/9.0*25f));

	    if(useGaussian){
		if(gaussianSize==3){
		    location= dilation5x5.getUniformLocation("gaussian");
		    dilation5x5.setInteger(location, 3);
		}  
		else{
		    location= dilation5x5.getUniformLocation("gaussian");
		    dilation5x5.setInteger(location, 5);
		}
	    }
	    else{
		location= dilation5x5.getUniformLocation("gaussian");
		dilation5x5.setInteger(location, 0);
	    }

	    //render a full-screen quad to cover all pixels
	    renderer.fullScreenQuad(1.0f);
	    render();
	    
	    dilation5x5.endShader();	    



	}
	else{

	    gl.glEnable(GL.GL_TEXTURE_2D);	    

	    dilation.startShader();

	    int location;

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    rightBackBottomTexture.bindDepthTexture();
	    location= dilation.getUniformLocation("depthTexture");
	    dilation.setTextureUnit(location, 0);

	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    lines.bindColorTexture();
	    location= dilation.getUniformLocation("lines");
	    dilation.setTextureUnit(location, 1);
	    
	    location= dilation.getUniformLocation("screenSize");
	    dilation.setFloatVec2(location, parent.width, parent.height);

	    location= dilation.getUniformLocation("color");
	    dilation.setFloatVec4(location, red, green, blue, alpha);

// 	    location= dilation.getUniformLocation("radius");
// 	    dilation.setFloat(location, (float)((int)width/2));

// 	    location= dilation.getUniformLocation("threshold");
// 	    float kernelSize= (int)width/2+1;
// 	    dilation.setInteger(location, (int)((kernelSize*kernelSize-1)-(width/(2.0*(float)kernelSize-1.0))*(kernelSize*kernelSize-1)));

	    
	    int radius= 0;
	    int maxWidth;
	    for(int i= 1; i<=width; i+=4){
		radius++;
	    }
	    maxWidth= 4*radius-1;
	    location= dilation.getUniformLocation("radius");
	    dilation.setFloat(location, radius);

	    float numPixels= (2*radius+1)*(2*radius+1);
	    location= dilation.getUniformLocation("threshold");
	    dilation.setInteger(location, (int)(numPixels-(width/maxWidth)*numPixels));
	    


	    if(useGaussian){
		if(gaussianSize==3){
		    location= dilation.getUniformLocation("gaussian");
		    dilation.setInteger(location, 3);
		}  
		else{
		    location= dilation.getUniformLocation("gaussian");
		    dilation.setInteger(location, 5);
		}
	    }
	    else{
		location= dilation.getUniformLocation("gaussian");
		dilation.setInteger(location, 0);
	    }

	    //render a full-screen quad to cover all pixels
	    renderer.fullScreenQuad(1.0f);
	    render();
	    
	    dilation.endShader();
	}

	gl.glActiveTexture(GL.GL_TEXTURE1);
	lines.unbind();
	gl.glActiveTexture(GL.GL_TEXTURE0);
	rightBackBottomTexture.unbind();

    }

    /**
     * Sobel edge detection.
     */
    private void findLines(boolean toTexture){
	
	renderer= (NPR)parent.g;
	gl= renderer.gl;

	if(toTexture){

	    sobelToTexture.startShader();

	    int location;
	
	    location= sobelToTexture.getUniformLocation("screenSize");
	    sobelToTexture.setFloatVec2(location, leftFrontTopTexture.getWidth(), leftFrontTopTexture.getHeight());

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    if(useGaussian) leftFrontTopGaussian.bindColorTexture();
	    else leftFrontTopTexture.bindColorTexture();
	    location= sobelToTexture.getUniformLocation("leftFrontTopTexture");
	    sobelToTexture.setTextureUnit(location, 0);

	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    if(useGaussian) rightBackBottomGaussian.bindColorTexture();
	    else rightBackBottomTexture.bindColorTexture();
	    location= sobelToTexture.getUniformLocation("rightBackBottomTexture");
	    sobelToTexture.setTextureUnit(location, 1);

	    gl.glActiveTexture(GL.GL_TEXTURE2);
	    rightBackBottomTexture.bindDepthTexture();
	    location= sobelToTexture.getUniformLocation("depthTexture");
	    sobelToTexture.setTextureUnit(location, 2);

	    location= sobelToTexture.getUniformLocation("useSmoothstep");
	    sobelToTexture.setBoolean(location, useSmoothstep);
	
	    location= sobelToTexture.getUniformLocation("smoothstepStart");
	    sobelToTexture.setFloat(location, smoothstepStart);
	
	    location= sobelToTexture.getUniformLocation("smoothstepEnd");
	    sobelToTexture.setFloat(location, smoothstepEnd);

	    location= sobelToTexture.getUniformLocation("color");
	    sobelToTexture.setFloatVec4(location, red, green, blue, alpha);
 
	    //render a full-screen quad to cover all pixels
	    renderer.fullScreenQuad(1.0f);

	    sobelToTexture.endShader();
	}
	else{
	    sobelToScreen.startShader();

	    int location;
	
	    location= sobelToScreen.getUniformLocation("screenSize");
	    sobelToScreen.setFloatVec2(location, parent.width, parent.height);	

	    gl.glActiveTexture(GL.GL_TEXTURE0);
	    if(useGaussian) leftFrontTopGaussian.bindColorTexture();
	    else leftFrontTopTexture.bindColorTexture();
	    location= sobelToScreen.getUniformLocation("leftFrontTopTexture");
	    sobelToScreen.setTextureUnit(location, 0);

	    gl.glActiveTexture(GL.GL_TEXTURE1);
	    if(useGaussian) rightBackBottomGaussian.bindColorTexture();
	    else rightBackBottomTexture.bindColorTexture();
	    location= sobelToScreen.getUniformLocation("rightBackBottomTexture");
	    sobelToScreen.setTextureUnit(location, 1);

	    gl.glActiveTexture(GL.GL_TEXTURE2);
	    rightBackBottomTexture.bindDepthTexture();
	    location= sobelToScreen.getUniformLocation("depthTexture");
	    sobelToScreen.setTextureUnit(location, 2);

	    location= sobelToScreen.getUniformLocation("useSmoothstep");
	    sobelToScreen.setBoolean(location, useSmoothstep);
	
	    location= sobelToScreen.getUniformLocation("smoothstepStart");
	    sobelToScreen.setFloat(location, smoothstepStart);
	
	    location= sobelToScreen.getUniformLocation("smoothstepEnd");
	    sobelToScreen.setFloat(location, smoothstepEnd);

	    location= sobelToScreen.getUniformLocation("color");
	    sobelToScreen.setFloatVec4(location, red, green, blue, alpha);

	    if(useGaussian){
		if(gaussianSize==3){
		    location= sobelToScreen.getUniformLocation("gaussian");
		    sobelToScreen.setInteger(location, 3);
		}  
		else{
		    location= sobelToScreen.getUniformLocation("gaussian");
		    sobelToScreen.setInteger(location, 5);
		}
	    }
	    else{
		location= sobelToScreen.getUniformLocation("gaussian");
		sobelToScreen.setInteger(location, 0);
	    }

	    //render a full-screen quad to cover all pixels
	    renderer.fullScreenQuad(1.0f);
	    render();

	    sobelToScreen.endShader();
	}

	gl.glActiveTexture(GL.GL_TEXTURE2);
	rightBackBottomTexture.unbind();
	gl.glActiveTexture(GL.GL_TEXTURE1);
	if(useGaussian) rightBackBottomGaussian.unbind();
	else rightBackBottomTexture.unbind();
	gl.glActiveTexture(GL.GL_TEXTURE0);
	if(useGaussian) leftFrontTopTexture.unbind();
	else leftFrontTopTexture.unbind();	
    }
}
